
// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: juntang

#include "coin2/exchange/bitget/order_v1/parser.h"

#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/bitget/api_order/native_private_client.h"
#include "coin2/exchange/bitget/order_v1/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::bitget::order_v1 {

namespace {

using namespace coin2::exchange::bitget::order_v1::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::bitget::symbology::BitgetCurrency;
using coin2::exchange::bitget::symbology::BitgetProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using TransferType = coin::proto::TransferRequestProto::TransferType;

inline int64_t GetTransferTimestamp(std::string time_str) {
  time_str.replace(10, 1, "T");
  return TimestampFromIso8601(time_str + ".000Z");
}

inline int64_t GetProcOrderId(const JsonValue& data) {
  int64_t proc_order_id = 0;
  if (data.HasMember("clientOrderId") && data["clientOrderId"].IsInt64()) {
    proc_order_id = stoll(data["clientOrderId"].GetString());
  }
  return proc_order_id;
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void SetOrderStatus(const std::string& status, ProductOrderElement* order) {
  if ((status == "init") || (status == "new") || (status == "partial_fill") || (status == "partial-fill")) {
    order->set_is_live(true);
  } else if (status == "cancelled") {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if ((status == "full_fill") || (status == "full-fill")) {
    order->set_is_live(false);
    order->set_fully_filled(true);
  } else {
    LOG(ERROR) << "[SetOrderStatus] Unknown order status: " << status;
  }
}

bool IsBitgetErrMessage(const JsonValue& doc) {
  if (doc.HasMember("code")) {
    return (std::string(doc["code"].GetString()) != "00000");
  }
  return false;
}

}  // namespace

void BitgetParser::ParseRestSpotAccount() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

bool BitgetParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  if (doc.IsObject() && IsBitgetErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }
  if (!doc.HasMember("data") || !doc["data"].IsArray()) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc["data"].GetArray()) {
    if (!data.HasMember("coinName") || !data.HasMember("available") || !data.HasMember("frozen") ||
        !data.HasMember("lock")) {
      continue;
    }
    auto current_available = stod(data["available"].GetString());
    auto current_frozen = stod(data["frozen"].GetString());
    auto current_lock = stod(data["lock"].GetString());
    CurrencyBalance balance;
    balance.set_currency_native(data["coinName"].GetString());
    balance.set_available(current_available);
    balance.set_hold(current_frozen + current_lock);
    balance.set_total(current_available + current_frozen + current_lock);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance BitgetParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Bitget");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
  }

  return acc_balance;
}

ProductOrderElement BitgetParser::ParseRestOrderInfo(const JsonValue& data) {
  ProductOrderElement order;
  std::string native_symbol = data["symbol"].GetString();
  if (!symcache_->has_product_native(native_symbol)) {
    return order;
  }
  order.set_market_type(MarketType::Spot);
  order.set_exchange_type(ExchangeType::Bitget);
  order.set_api_version("v1");
  order.set_exchange_order_id(data["orderId"].GetString());
  order.set_symbol(native_symbol);
  order.set_price(stod(data["price"].GetString()));
  order.set_qty(stod(data["quantity"].GetString()));
  order.set_proc_order_id(GetProcOrderId(data));
  order.set_side(GetOrderSide(data["side"].GetString()));
  order.set_order_type(GetOrderType(data["orderType"].GetString()));
  order.set_timestamp(GetCurrentTimestamp());
  SetOrderStatus(data["status"].GetString(), &order);
  return order;
}

void BitgetParser::ParseRestOpenOrders() { ParseRestOrderInfoList(); }

void BitgetParser::ParseRestAllOrders() { ParseRestOrderInfoList(); }

void BitgetParser::ParseRestOrderInfoList() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsBitgetErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    return;
  }
  if (!data.HasMember("data") || !data["data"].IsArray()) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  bool product_order_bundle_found = false;
  for (const auto& info : data["data"].GetArray()) {
    auto new_order = ParseRestOrderInfo(info);

    if ((new_order.is_live()) && new_order.has_market_type()) {
      auto* order = product_order_bundle->add_each_order();
      order->MergeFrom(new_order);
      product_order_bundle_found = true;
    }
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void BitgetParser::ParseRestFillInfoList() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestFillOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsBitgetErrMessage(data)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data);
    return;
  }
  if (!data.HasMember("data") || !(data["data"].IsArray() && data["data"].Size() > 0)) {
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();

  bool product_fill_bundle_found = false;
  for (const auto& info : data["data"].GetArray()) {
    auto* fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["symbol"].GetString());
    fill->set_fill_timestamp(std::stoll(info["cTime"].GetString()) * 1000000LL);
    fill->set_exchange_order_id(info["orderId"].GetString());
    fill->set_price(stod(info["fillPrice"].GetString()));
    fill->set_qty(stod(info["fillQuantity"].GetString()));
    fill->set_fill_id(info["fillId"].GetString());

    fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    const std::string fee_currency = info["feeCcy"].GetString();
    if (!fee_currency.empty()) {
      fill->set_fee_currency(fee_currency);
      if (info.HasMember("fees") && !info["fees"].IsNull()) {
        const std::string fee = info["fees"].GetString();
        if (!fee.empty()) {
          fill->set_fee(-stod(fee));
        }
      }
    }
    product_fill_bundle_found = true;
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void BitgetParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BitgetSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  const auto http_code = http_context->res.result_int();
  if (http_code == 429) {
    LOG(ERROR) << "[BitgetSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_exchange_error_msg(http_context->res.body().data());
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BitgetSubmitOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("code") && json.HasMember("msg")) {
    auto error_code = GetOrderErrorCode(json["code"], json["msg"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[BitgetSubmitOrder] Error Response: \n" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  if (json.HasMember("data") && json["data"].HasMember("orderId")) {
    resp->set_success(true);
    resp->set_exchange_order_id(json["data"]["orderId"].GetString());
  }
  resp->set_success(true);
}

void BitgetParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BitgetCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }
  const auto http_code = http_context->res.result_int();
  if (http_code == 429) {
    LOG(ERROR) << "[BitgetCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_exchange_error_msg(http_context->res.body().data());
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BitgetCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("data") && (!json["data"].IsNull())) {
    resp->set_exchange_order_id(std::string(json["data"].GetString()));
  }

  if (json.HasMember("code") && json.HasMember("msg")) {
    auto error_code = GetOrderErrorCode(json["code"], json["msg"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[BitgetCancelOrder] Error Response" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(false);
}

bool BitgetParser::IsOrderUpdate(const JsonDocument& json) {
  if (json.HasMember("arg")) {
    return (json["arg"]["channel"] == "orders");
  }
  return false;
}

bool BitgetParser::IsAccountUpdate(const JsonDocument& json) {
  if (json.HasMember("arg")) {
    return (json["arg"]["channel"] == "account");
  }
  return false;
}

void BitgetParser::ParseWsMessage() {
  JsonDocument document;

  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.HasMember("arg")) {
    return;
  }

  if (IsOrderUpdate(document)) {
    ParseWsOrderInfoList(document);
    ParseWsFillInfoList(document);
  }
}

//not used here, because lack of hold and total value
void BitgetParser::ParseWsAccountInfoList(const JsonDocument& document) {
  auto acc_balance = result_.mutable_account_balance();
  acc_balance->Clear();
  acc_balance->set_exchange("Bitget");
  acc_balance->set_market_type("Spot");

  if (!document.HasMember("data")) {
    return;
  }

  bool acc_balance_found = false;
  for (const auto& data : document["data"].GetArray()) {
    if (!data.HasMember("coinId") || !data.HasMember("available")) {
      continue;
    }
    if (!symcache_->has_currency_native(data["coinName"].GetString())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(data["coinName"].GetString());
    auto* currency_balance = acc_balance->add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    auto avail = stod(data["available"].GetString());
    currency_balance->set_available(avail);
    acc_balance_found = true;
  }

  if (acc_balance_found) {
    result_.set_type(MsgType::ACCOUNT_BALANCE);
  }
}

void BitgetParser::ParseWsOrderInfoList(const JsonDocument& document) {
  /*
  {
    "event": "subscribe",
    "arg": {
        "channel": "orders",
        "instType": "spbl",
        "instId": "BTCUSDT_SPBL"
    }
  }
  */
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;

  if (!document.HasMember("data")) {
    return;
  }

  for (const auto& data : document["data"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    order->set_exchange_type(ExchangeType::Bitget);
    order->set_market_type(MarketType::Spot);
    order->set_api_version("v1");
    order->set_exchange_order_id(data["ordId"].GetString());
    order->set_symbol(data["instId"].GetString());
    order->set_price(stod(data["px"].GetString()));
    order->set_qty(std::fabs(stod(data["sz"].GetString())));
    order->set_proc_order_id(stoll(data["clOrdId"].GetString()));
    order->set_side(GetOrderSide(data["side"].GetString()));
    order->set_order_type(GetOrderType(data["ordType"].GetString()));
    order->set_timestamp(GetCurrentTimestamp());
    SetOrderStatus(data["status"].GetString(), order);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void BitgetParser::ParseWsFillInfoList(const JsonDocument& document) {
  if (!document.HasMember("data")) {
    return;
  }
  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;

  for (const auto& data : document["data"].GetArray()) {
    if (!data.HasMember("tradeId")) {
      LOG(INFO) << "!data.HasMember(tradeId)";
      continue;
    }

    auto* fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(data["instId"].GetString());
    fill->set_fill_timestamp(std::stoll(data["fillTime"].GetString()) * 1000000LL);
    fill->set_exchange_order_id(data["ordId"].GetString());
    if (std::string(data["clOrdId"].GetString()).empty()) {
      fill->set_proc_order_id(stoll(data["clOrdId"].GetString()));
    } else {
      fill->set_proc_order_id(stoll(data["clOrdId"].GetString()));
    }
    fill->set_price(stod(data["fillPx"].GetString()));
    fill->set_qty(stod(data["fillSz"].GetString()));
    fill->set_fill_id(data["tradeId"].GetString());
    if (std::string_view(data["execType"].GetString()) == "T") {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else if (std::string_view(data["execType"].GetString()) == "M") {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }
    if (data.HasMember("fillFeeCcy") && !data["fillFeeCcy"].IsNull()) {
      const std::string fee_currency = data["fillFeeCcy"].GetString();
      if (!fee_currency.empty()) {
        fill->set_fee_currency(fee_currency);
        if (data.HasMember("fillFee") && !data["fillFee"].IsNull()) {
          const std::string fee = data["fillFee"].GetString();
          if (!fee.empty()) {
            fill->set_fee(-stod(fee));
          }
        }
      }
    }
    product_fill_bundle_found = true;
  }

  LOG(INFO) << "product_fill_bundle_found" << product_fill_bundle_found;
  if (product_fill_bundle_found) {
    LOG(INFO) << "MsgType::ORDER_FILL";
    result_.set_type(MsgType::ORDER_FILL);
  }
}

auto BitgetParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ACCOUNTS:
      ParseRestSpotAccount();
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestAllOrders();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrders();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    default: {
      LOG(ERROR) << "[ParseRawMessage] Unknown query type\n" << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }
  return result_;
}

}  // namespace coin2::exchange::bitget::order_v1
